Skip to content

Validate Treelite model inputs during import - #104

Merged
rapids-bot[bot] merged 11 commits into
rapidsai:release/26.06from
chyunsu3:validate_treelite
May 21, 2026
Merged

Validate Treelite model inputs during import#104
rapids-bot[bot] merged 11 commits into
rapidsai:release/26.06from
chyunsu3:validate_treelite

Conversation

@chyunsu3

@chyunsu3 chyunsu3 commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Ports the Treelite input validation work from rapidsai/cuml#8016 to nvforest.

Adds import-time checks for Treelite model values that would otherwise be packed into narrower nvforest node metadata or categorical storage without validation. The branch also consolidates model import failures under model_import_error and adds context to import errors so failures identify the source Treelite tree and node.

Summary

  • Validates node feature IDs before packing them into node metadata.
  • Validates categorical split values and categorical storage bounds before constructing the final forest.
  • Checks floating-point downcasts for postprocessing constants instead of relying on unchecked narrowing.
  • Hardens shared helpers used by the import path, including bitset writes and ceildiv.
  • Adds invalid-input coverage for Treelite import and decision_forest_builder.
  • Renames local test variables from fil_model to nvforest_model.

Follow-up to NVIDIA/cuml#8016

@chyunsu3
chyunsu3 requested a review from a team as a code owner April 30, 2026 00:44
@chyunsu3 chyunsu3 added improvement Improves an existing functionality non-breaking Introduces a non-breaking change labels Apr 30, 2026
@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Prevented out-of-bounds writes by ignoring out-of-range bitset indices and added broader bounds validation across model import.
  • New Features

    • Better error reporting with contextual messages for import failures and explicit runtime checks for numeric truncation and invalid postprocessing constants.
  • Refactor

    • Strengthened type-safety and index/feature constraints to enforce valid model layouts.
  • Tests

    • Added tests covering invalid categorical/feature inputs and floating-point narrowing errors.

Walkthrough

Strengthens import/build pipeline: add compile-time type constraints and bounds checks (bitset, node, ceildiv), safe floating-point narrowing, categorical sizing and root/node index validation in the builder, per-node importer error context, and expanded invalid-input tests.

Changes

Import & Validation updates

Layer / File(s) Summary
Type Safety & Compile-Time Checks
cpp/include/nvforest/detail/bitset.hpp, cpp/include/nvforest/detail/node.hpp, cpp/include/nvforest/detail/raft_proto/ceildiv.hpp, cpp/include/nvforest/exceptions.hpp
Adds static_assert requirements for unsigned/integral types; bitset::set/clear include bounds checks; node constructors drop HOST DEVICE and add runtime feature bounds validation; model_import_error constructors/what() adjusted.
Decision Forest Builder Validations
cpp/include/nvforest/detail/decision_forest_builder.hpp
Introduces safe_cast_floating_point with truncation error, enforces categorical/index type constraints, overflow-safe (max_category + 1) handling, and adds runtime invariant checks in get_decision_forest; uses validated casts for decision_forest_t construction.
Treelite Importer: error context & inline APIs
cpp/include/nvforest/treelite_importer.hpp, cpp/include/nvforest/detail/degenerate_trees.hpp
Wraps per-node construction in try/catch to rethrow model_import_error with Tree <id>, Node <id> context; refines leaf-vector handling; marks importer functions and convert_degenerate_trees as inline in headers.
Tests & CMake
cpp/tests/CMakeLists.txt, cpp/tests/treelite_importer.cpp, cpp/tests/treelite_importer_invalid_inputs.cpp, cpp/tests/decision_forest_builder_invalid_inputs.cpp
Adds new invalid-input tests for categorical offsets, bitset extents, feature-id bounds, safe floating-point casts, and postprocessor constant validation; updates existing tests variable names and adds new compilation units to the test target.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • rapidsai/nvforest#119: Overlaps on exception/Importer changes and model_import_error/treelite_importer adjustments.

Suggested reviewers

  • bdice
  • dantegd
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely summarizes the main change: adding validation of Treelite model inputs during the import process.
Description check ✅ Passed The description is well-related to the changeset, explaining the validation work ported from cuml, the specific checks added, and referencing the source PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@csadorf csadorf self-assigned this May 20, 2026
@csadorf
csadorf force-pushed the validate_treelite branch from ea44548 to b0ae86e Compare May 20, 2026 20:08
@csadorf
csadorf changed the base branch from main to release/26.06 May 20, 2026 20:09
@csadorf csadorf changed the title Port rapidsai/cuml#8016, Treelite input validation Validate Treelite model inputs during import May 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cpp/include/nvforest/detail/node.hpp (1)

109-130: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Document GPU restriction and provide migration path for host-only constructor change.

The constructors at lines 109–130 lack HOST DEVICE while accessor methods (lines 140+) retain it, breaking the device-callable contract. The inline comment "Assumption: Node construction occurs on the host" only appears in code, not in the public Doxygen documentation. Add Doxygen comments to these constructors documenting the host-only requirement, GPU restrictions, and provide a recommended migration path for any existing device-side construction code. The class-level docs should also clarify that while node accessors are device-callable, construction is host-only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/nvforest/detail/node.hpp` around lines 109 - 130, Update the
public documentation to make the host-only restriction explicit: add Doxygen
comments to both node constructors (the two overloads named node(...)) stating
they are host-only (no HOST_DEVICE), list the GPU restriction and runtime
consequences, and provide a clear migration path (e.g., construct nodes on host
and transfer to device, or provide a device-friendly factory) for any existing
device-side construction code; also update the class-level Doxygen to clarify
that accessor methods remain device-callable while construction is host-only and
point readers to the recommended migration approach and any helper
utilities/functions to perform host-to-device transfer.
🧹 Nitpick comments (2)
cpp/include/nvforest/detail/node.hpp (1)

217-221: ⚡ Quick win

Update the node docs to describe the new validation behavior.

The public comment above node still says construction does “NO error checking” and will “silently” build an incorrect node, but construct_metadata() now rejects out-of-range feature values and throws model_import_error. Please sync the Doxygen with the actual constructor behavior, including the feature bound and thrown exception.

As per coding guidelines, "For public header files (C++ API): Verify parameter descriptions match actual types/behavior" and "Flag API changes that may need corresponding documentation updates".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/nvforest/detail/node.hpp` around lines 217 - 221, Update the
Doxygen comment above the node type to reflect that construct_metadata() now
validates the feature value and will throw model_import_error for out-of-range
values: document the feature bound (must be <= FEATURE_MASK), state that the
constructor/construct_metadata() performs runtime checking rather than “NO error
checking” and will raise model_import_error on violation, and ensure parameter
descriptions and exception tags match the actual types/behavior (mention
construct_metadata(), FEATURE_MASK, and model_import_error).
cpp/include/nvforest/exceptions.hpp (1)

15-17: ⚡ Quick win

Broaden the public docs for model_import_error.

After folding validation failures into model_import_error, this type no longer represents only import-time failures. node::construct_metadata() now throws it for generic model validation as well, so the current /** Exception indicating model import failed */ description is too narrow for downstream users. Please update the public API docs/comments to reflect the broader contract.

As per coding guidelines, "For public header files (C++ API): Check if new public functions/classes have documentation comments" and "Flag API changes that may need corresponding documentation updates".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/nvforest/exceptions.hpp` around lines 15 - 17, Update the
documentation comment for the exception type model_import_error in
exceptions.hpp to reflect that it is used for both import-time failures and
broader model validation errors (e.g., those thrown by
node::construct_metadata()), not just import-only failures; edit the public
header comment above the model_import_error class/constructor to describe this
broader contract and ensure it matches coding guidelines for public C++ API
docs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/include/nvforest/detail/decision_forest_builder.hpp`:
- Around line 112-120: The code chooses categorical backing based on
max_num_categories_ but builds the bitset with max_cat_plus_one, risking
out-of-bounds writes; change the storage-mode decision to use the actual bitset
width (use max_cat_plus_one instead of max_num_categories_) so the condition
becomes if (max_cat_plus_one > bin_width) and compute bins_required/resize using
max_cat_plus_one consistently (ensure node_value, categorical_storage_,
set_storage are derived from that width), and additionally add a pre-inference
model validation call that verifies tree depth, node counts and feature indices
(per coding guidelines) before any bitset/set(cat_index) operations run.

In `@cpp/include/nvforest/treelite_importer.hpp`:
- Around line 261-269: The code accesses output[0] without checking for empty
outputs, causing an out-of-bounds read for malformed models; in the
node.is_leaf() branch (using node.get_output(), builder.set_output_size(),
builder.add_leaf_vector_node, builder.add_node and forest_model_t::io_type), add
a guard for output.empty() before any indexing and handle it deterministically
(e.g. throw a descriptive exception or skip/mark the node as invalid) so you
never call output[0] when output.size()==0; keep existing behavior for
output.size()>=1 and route multi-element outputs to add_leaf_vector_node as
before.

---

Outside diff comments:
In `@cpp/include/nvforest/detail/node.hpp`:
- Around line 109-130: Update the public documentation to make the host-only
restriction explicit: add Doxygen comments to both node constructors (the two
overloads named node(...)) stating they are host-only (no HOST_DEVICE), list the
GPU restriction and runtime consequences, and provide a clear migration path
(e.g., construct nodes on host and transfer to device, or provide a
device-friendly factory) for any existing device-side construction code; also
update the class-level Doxygen to clarify that accessor methods remain
device-callable while construction is host-only and point readers to the
recommended migration approach and any helper utilities/functions to perform
host-to-device transfer.

---

Nitpick comments:
In `@cpp/include/nvforest/detail/node.hpp`:
- Around line 217-221: Update the Doxygen comment above the node type to reflect
that construct_metadata() now validates the feature value and will throw
model_import_error for out-of-range values: document the feature bound (must be
<= FEATURE_MASK), state that the constructor/construct_metadata() performs
runtime checking rather than “NO error checking” and will raise
model_import_error on violation, and ensure parameter descriptions and exception
tags match the actual types/behavior (mention construct_metadata(),
FEATURE_MASK, and model_import_error).

In `@cpp/include/nvforest/exceptions.hpp`:
- Around line 15-17: Update the documentation comment for the exception type
model_import_error in exceptions.hpp to reflect that it is used for both
import-time failures and broader model validation errors (e.g., those thrown by
node::construct_metadata()), not just import-only failures; edit the public
header comment above the model_import_error class/constructor to describe this
broader contract and ensure it matches coding guidelines for public C++ API
docs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fa8dadf8-cde1-4771-840c-bf8601df6dce

📥 Commits

Reviewing files that changed from the base of the PR and between ea44548 and b0ae86e.

📒 Files selected for processing (11)
  • cpp/include/nvforest/detail/bitset.hpp
  • cpp/include/nvforest/detail/decision_forest_builder.hpp
  • cpp/include/nvforest/detail/degenerate_trees.hpp
  • cpp/include/nvforest/detail/node.hpp
  • cpp/include/nvforest/detail/raft_proto/ceildiv.hpp
  • cpp/include/nvforest/exceptions.hpp
  • cpp/include/nvforest/treelite_importer.hpp
  • cpp/tests/CMakeLists.txt
  • cpp/tests/decision_forest_builder_invalid_inputs.cpp
  • cpp/tests/treelite_importer.cpp
  • cpp/tests/treelite_importer_invalid_inputs.cpp
✅ Files skipped from review due to trivial changes (1)
  • cpp/tests/treelite_importer.cpp

Comment thread cpp/include/nvforest/detail/decision_forest_builder.hpp
Comment thread cpp/include/nvforest/treelite_importer.hpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
cpp/include/nvforest/detail/decision_forest_builder.hpp (1)

116-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject categorical nodes when max_num_categories_ is unset.

If this builder is default-constructed, add_categorical_node() reaches Line 116 with max_num_categories_ == 0, skips external storage even when max_cat_plus_one > bin_width, and get_decision_forest() later reports has_categorical = false. That leaves the forest in an inconsistent state and can under-allocate the categorical bitset backing.

Suggested minimal fix
+    if (max_num_categories_ == index_type{}) {
+      throw model_import_error{
+        "Categorical nodes require max_num_categories to be configured"};
+    }
     if (max_num_categories_ > bin_width) {
       node_value         = categorical_storage_.size();
       auto bins_required = raft_proto::ceildiv(max_cat_plus_one, bin_width);
       categorical_storage_.push_back(max_cat_plus_one);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/include/nvforest/detail/decision_forest_builder.hpp` at line 116, The
builder currently allows add_categorical_node() to treat max_num_categories_ ==
0 as if it were large enough, causing has_categorical to become inconsistent and
under-allocating the categorical bitset; update the conditional in
add_categorical_node() (the check using max_num_categories_ and bin_width) to
explicitly reject the case where max_num_categories_ is unset by requiring
max_num_categories_ != 0 (e.g. change the condition to require
max_num_categories_ != 0 && max_num_categories_ > bin_width) and ensure any code
paths that rely on max_cat_plus_one, get_decision_forest(), or the categorical
bitset backing respect this rejection so categorical nodes are only accepted
when a positive max_num_categories_ is configured.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@cpp/include/nvforest/detail/decision_forest_builder.hpp`:
- Line 116: The builder currently allows add_categorical_node() to treat
max_num_categories_ == 0 as if it were large enough, causing has_categorical to
become inconsistent and under-allocating the categorical bitset; update the
conditional in add_categorical_node() (the check using max_num_categories_ and
bin_width) to explicitly reject the case where max_num_categories_ is unset by
requiring max_num_categories_ != 0 (e.g. change the condition to require
max_num_categories_ != 0 && max_num_categories_ > bin_width) and ensure any code
paths that rely on max_cat_plus_one, get_decision_forest(), or the categorical
bitset backing respect this rejection so categorical nodes are only accepted
when a positive max_num_categories_ is configured.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 49aeafd3-50ed-43f4-83e5-330758a8b26b

📥 Commits

Reviewing files that changed from the base of the PR and between a0a32e4 and f19420c.

📒 Files selected for processing (1)
  • cpp/include/nvforest/detail/decision_forest_builder.hpp

@chyunsu3

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit a2d56e1 into rapidsai:release/26.06 May 21, 2026
66 checks passed
@csadorf csadorf linked an issue May 21, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CMake CUDA/C++ improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port over rapidsai/cuml#8016

3 participants